import random
from datetime import datetime
import requests
import re
# 从项目设置中导入用户代理列表和全局IP池变量
from myproject.settings import USER_AGENT_LIST, ipPool


class RandomUserAgent(object):
    """
    Scrapy 下载中间件：实现随机 User-Agent 和动态代理 IP 池管理。
    功能包括：
      - 随机设置请求头中的 User-Agent
      - 使用轮询策略从 IP 池中选择代理
      - 对失败请求自动更换代理并重试（最多 max_retries 次）
      - 自动刷新和补充 IP 池以维持最小数量
      - 记录错误信息并写入日志或文件
    """

    def __init__(self):
        """
        初始化中间件状态。
        """
        self.max_retries = 10  # 每个 URL 最多重试 10 次
        self.current_proxy_index = -1  # 当前使用的代理在 ipPool 中的索引，初始为 -1，第一次调用 get_next_proxy 时会变为 0
        self.min_ip_pool_size = 14  # IP 池最小容量，低于此值将自动补充

        # 初始化 IP 池：从远程服务获取一批可用代理
        self.refresh_ip_pool()
        print("初始化完毕")

    def process_request(self, request, spider):
        """
        在每个请求发出前调用。
        负责设置：
          - 随机 User-Agent
          - 代理（proxy）
          - 初始化重试次数和错误记录字段
        """
        # 从预定义列表中随机选择一个 User-Agent
        self.ua = random.choice(USER_AGENT_LIST)
        request.headers['User-Agent'] = self.ua  # 设置请求头

        # 打印时间戳用于调试请求流程
        print("设置代理前", datetime.now())

        # 获取下一个代理 IP（轮询方式），并设置到 request.meta['proxy']
        request.meta['proxy'] = self.get_next_proxy()
        print("设置代理后", datetime.now())

        # 初始化重试计数器（若尚未设置）
        if 'retries' not in request.meta:
            request.meta['retries'] = 0

        # 初始化错误信息列表（用于记录每次失败的原因）
        if 'error_info' not in request.meta:
            request.meta['error_info'] = []

    def process_response(self, request, response, spider):
        """
        在收到响应后调用。
        判断响应是否有效，若无效则触发重试机制。
        """
        print("接收数据后", datetime.now())

        # 获取当前重试次数
        retries = request.meta.get('retries', 0)

        # 判断响应是否异常：
        #   - 状态码非 200
        #   - 页面包含反爬提示（如“身份核实”）
        #   - 特定文本内容（疑似被定向到验证页）
        #   - URL 被重定向到错误页面（如东方财富股吧的错误页）
        if (response.status != 200 or
            "身份核实" in response.text or
            "A股：大家做好心理准备了，A股，很可能要迎来大级别的行情" in response.text or
            response.url == 'https://guba.eastmoney.com/error?type=1'):

            # 记录本次失败原因
            error_info = f"Status code: {response.status}, URL: {request.url}"
            request.meta['error_info'].append(error_info)

            # 输出警告日志
            spider.logger.warning(error_info)
            print("问题页面", response.text[:500])  # 只打印前500字符避免刷屏

            # 将当前使用的代理标记为失效，并从 IP 池中移除
            self.replace_ip(request.meta['proxy'])

            # 返回一个新的重试请求
            return self._retry(request, spider, retries)

        # 响应正常，继续向后传递
        print("转发数据前", datetime.now())
        return response

    def process_exception(self, request, exception, spider):
        """
        当请求过程中发生异常（如连接超时、DNS 错误等）时调用。
        记录异常并尝试重试。
        """
        retries = request.meta.get('retries', 0)

        # 构造异常信息
        error_info = f"Exception: {str(exception)}, URL: {request.url}"
        request.meta['error_info'].append(error_info)

        # 输出警告日志
        spider.logger.warning(error_info)
        print("请求异常，重试")

        # 打印完整堆栈跟踪，便于调试网络异常
        import traceback
        spider.logger.debug(traceback.format_exc())

        # 将当前代理标记为失效
        self.replace_ip(request.meta['proxy'])

        # 触发重试
        return self._retry(request, spider, retries)

    def _retry(self, request, spider, retries):
        """
        内部方法：生成一个重试用的新请求。
        实现指数退避（当前注释）、更新代理、增加重试次数。
        """
        # 检查是否已达到最大重试次数
        if retries < self.max_retries:
            new_retries = retries + 1
            print(f"重试次数：{new_retries}/{self.max_retries}")

            # TODO: 可启用指数退避（当前被注释）
            # backoff = 2 ** new_retries
            # time.sleep(backoff)  # 需要 import time

            # 创建原请求的副本（保留所有参数）
            retry_req = request.copy()

            # 更新重试次数
            retry_req.meta['retries'] = new_retries

            # 关键设置：dont_filter=True 避免 Scrapy 去重机制过滤掉重试请求
            retry_req.dont_filter = True

            # 更换代理（防止使用同一个失效 IP 无限重试）
            retry_req.meta['proxy'] = self.get_next_proxy()

            # 输出日志
            spider.logger.info(f"Retrying {request.url} (attempt {new_retries}/{self.max_retries})")

            # 返回新的请求对象，Scrapy 将重新调度执行
            return retry_req

        else:
            # 超过最大重试次数，放弃请求

            # 将失败 URL 写入本地文件，便于后期分析或重新抓取
            with open('errorurl.txt', 'a', encoding='utf-8') as f:
                f.write(request.url + '\n')

            # 汇总所有历史错误信息
            error_info = request.meta.get('error_info', [])
            spider.logger.error(
                f"Gave up retrying {request.url} after {self.max_retries} attempts. "
                f"Errors: {', '.join(error_info)}"
            )
            return None  # 返回 None 表示放弃该请求

    def refresh_ip_pool(self):
        """
        刷新整个 IP 池：清空旧 IP，从远程接口获取一批新代理。
        使用 51daili.com 的 API 接口获取高匿名 HTTP 代理。
        """
        print('-------------刷新IP池------------------')
        global ipPool
        ipPool.clear()  # 清空现有 IP 列表

        try:
            # 51代理平台的认证信息（示例账号）
            hwmhwm = {
                "uid": "61142",
                "accessName": "hwmhwm",
                "accessPassword": "E5F65B43AB0BBE91CA0BEE7F11B4081C61142"
            }  # 黄账号
            qwsdq = {
                "uid": "62399",
                "accessName": "qwsdq",
                "accessPassword": "E8538B779F3D0A7379C1302AD4C0055B"
            }  # 翟账号

            # 当前使用的账号（可切换）
            currentUse = qwsdq

            # 计划获取的 IP 数量，确保至少达到最小池大小
            fetch_count = self.min_ip_pool_size

            # 调用 51daili 的 API 获取 JSON 格式的代理列表
            # 参数说明：
            #   qty: 获取数量
            #   time: 有效期（分钟）
            #   port=1: 返回带端口格式
            #   format=json: 返回 JSON
            #   skey=autoaddwhiteip: 自动添加当前服务器 IP 到白名单
            ips = requests.get(
                f'http://bapi.51daili.com/getapi2?'
                f'linePoolIndex=-1&packid=2&time=11&qty={fetch_count}&port=1&'
                f'format=json&field=ipport&dt=1&ct=1&usertype=17&'
                f'uid={currentUse["uid"]}&accessName={currentUse["accessName"]}&'
                f'accessPassword={currentUse["accessPassword"]}&skey=autoaddwhiteip',
                headers={'User-Agent': random.choice(USER_AGENT_LIST)},
                timeout=10
            )

            # 检查 HTTP 响应状态（如 404、500 等）
            ips.raise_for_status()

            # 使用正则提取 JSON 中的 "ip":"xxx.xxx.xxx.xxx:port" 字段
            ip_port_list = re.findall(r'"ip":"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+)"', ips.text)

            # 转换为 Scrapy 可用的代理格式：http://ip:port
            ipPool = [f"http://{ipport}" for ipport in ip_port_list]
            print(f"成功获取 {len(ipPool)} 个代理IP", ipPool)

            # 补充 IP 池至最小容量（防止获取数量不足）
            self.ensure_min_ip_pool_size()

        except Exception as e:
            # 异常处理：网络错误、解析失败等
            print(f"获取IP代理时出错: {e}")
            ipPool = []  # 清空后尝试补充
            self.ensure_min_ip_pool_size()  # 确保池不为空

    def replace_ip(self, ip):
        """
        当某个代理失效时，将其从 IP 池中移除，并补充一个新 IP。
        """
        global ipPool
        if ip in ipPool:
            ipPool.remove(ip)
            print(f"移除失效代理IP: {ip}")

            # 补充新 IP 以维持池大小
            self.ensure_min_ip_pool_size()

    def ensure_min_ip_pool_size(self):
        """
        确保 IP 池中的代理数量不低于 min_ip_pool_size。
        若不足，则逐个补充，直到满足要求或无法获取新 IP。
        """
        global ipPool

        # 循环补充，直到达到最小容量
        while len(ipPool) < self.min_ip_pool_size:
            try:
                # 再次调用 51daili API 获取单个新 IP（更精细控制）
                ips = requests.get(
                    'http://bapi.51daili.com/getapi2?'
                    'linePoolIndex=-1&packid=2&time=11&qty=1&port=1&'
                    'format=json&field=ipport&dt=1&ct=1&usertype=17&'
                    'uid=62399&accessName=qwsdq&accessPassword=E8538B779F3D0A7379C1302AD4C0055B&'
                    'skey=autoaddwhiteip',
                    headers={'User-Agent': random.choice(USER_AGENT_LIST)},
                    timeout=10
                )
                ips.raise_for_status()

                # 提取 IP:port
                ip_port_list = re.findall(r'"ip":"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+)"', ips.text)

                if ip_port_list:
                    new_ip = f"http://{ip_port_list[0]}"
                    if new_ip not in ipPool:  # 避免重复添加
                        ipPool.append(new_ip)
                        print(f"补充新代理IP: {new_ip}")
                    else:
                        print("获取到重复IP，跳过")
                else:
                    print("未能获取到有效IP")
                    break  # 避免无限循环

            except Exception as e:
                print(f"获取IP代理时出错: {e}")
                # 极端情况：若 IP 池为空，提供一个本地占位符（避免程序崩溃）
                if not ipPool:
                    ipPool = ['http://127.0.0.1:8080']
                break  # 出错后停止补充

    def get_next_proxy(self):
        """
        轮询策略：按顺序从 IP 池中取出下一个代理。
        使用取模实现循环。
        """
        # 若 IP 池太小，先补充
        if len(ipPool) < self.min_ip_pool_size:
            self.ensure_min_ip_pool_size()

        # 使用轮询（Round Robin）策略选择代理
        self.current_proxy_index = (self.current_proxy_index + 1) % len(ipPool)
        return ipPool[self.current_proxy_index]